package jmine.tec.services.export.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import jmine.tec.services.api.export.DefaultExportAction;
import jmine.tec.services.api.export.EntityExporter;
import jmine.tec.services.api.export.EntityExporterRepository;
import jmine.tec.services.api.export.ExportAction;
import jmine.tec.services.api.export.ExportActor;
import jmine.tec.services.api.export.ExportTarget;
import jmine.tec.services.api.export.ExporterKey;
import jmine.tec.services.api.export.ReferenceMap;
import jmine.tec.services.io.ServiceBean;

import org.apache.commons.lang.builder.HashCodeBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implementação default da interface ExportActor
 * 
 * @author lundberg
 */
public class ExportActorImpl implements ExportActor {

    private static final Logger LOG = LoggerFactory.getLogger(ExportActorImpl.class);

    private EntityExporterRepository exporterRepository;

    /**
     * {@inheritDoc}
     */
    public List<ServiceBean> export(List<?> targets) {
        LOG.debug(String.format("Iniciando exportação de %s entidades.", targets.size()));
        List<ServiceBean> result = new ArrayList<ServiceBean>();
        ReferenceMap referenceMap = new ReferenceMapImpl();
        Set<ExportedKey> alreadyExported = new HashSet<ExportedKey>();
        for (Object target : targets) {
            result.addAll(this.doExport(alreadyExported, referenceMap, this.exportTarget(target)));
        }
        LOG.debug(String.format("Exportação terminada. %s serviços gerados.", result.size()));
        return result;
    }

    /**
     * {@inheritDoc}
     */
    public boolean canExport(ExportAction action, Class<?> entityClass) {
        return this.exporterRepository.hasExporterFor(new ExporterKey(action, entityClass));
    }

    /**
     * {@inheritDoc}
     */
    public void register(Collection<? extends EntityExporter<?>> registers) {
        this.exporterRepository.register(registers);
    }

    /**
     * Exporta a entidade dada, gerando uma lista de ServiceBeans. Para evitar a repetição na exportação de entidades (o que pode ocorrer no
     * caso de dependencias comuns entre 2 objetos exportados, por exemplo), cada instancia é exportada apenas uma vez. Caso seja necessário
     * exportar duas vezes um objeto com os mesmos parâmetros, devem ser exportadas duas instancias distintas.
     * 
     * @param alreadyExported alreadyExported
     * @param referenceMap referenceMap
     * @param target target
     * @return List de ServiceBean
     */
    private List<ServiceBean> doExport(Set<ExportedKey> alreadyExported, ReferenceMap referenceMap, ExportTarget target) {
        List<ServiceBean> result = new ArrayList<ServiceBean>();
        this.exportEntities(alreadyExported, result, referenceMap, target);
        return result;
    }

    /**
     * Encontra o exporter adequado para a entidade. Este método nunca retorna null, caso um exporter não seja encontrado isso será
     * considerado uma falha de codificação e será jogada uma RuntimeException explicando a causa.
     * 
     * @param target target
     * @return EntityExporter da entidade
     */
    @SuppressWarnings("unchecked")
    private EntityExporter<Object> findEntityExporter(ExportTarget target) {
        return (EntityExporter<Object>) this.exporterRepository.findEntityExporter(new ExporterKey(target));
    }

    /**
     * Adiciona à lista as entidades que devem ser exportadas antes desta entidade (recursivamente), a entidade em si e em seguida as
     * entidades que devem ser exportadas após (resursivamente). Caso a entidade não esteja no mapa alreadyExported, ela será incluída. Caso
     * já esteja, o método retornará sem fazer nada.
     * 
     * @param alreadyExported alreadyExported
     * @param export export
     * @param referenceMap referenceMap
     * @param target target
     */
    private void exportEntities(Set<ExportedKey> alreadyExported, List<ServiceBean> export, ReferenceMap referenceMap, ExportTarget target) {
        if (alreadyExported.contains(new ExportedKey(target))) {
            return;
        } else {
            alreadyExported.add(new ExportedKey(target));
        }
        EntityExporter<Object> exporter = this.findEntityExporter(target);
        for (Object before : exporter.exportBefore(target.getEntity())) {
            this.exportEntities(alreadyExported, export, referenceMap, this.exportTarget(before));
        }
        export.addAll(exporter.export(referenceMap, target.getEntity()));
        for (Object after : exporter.exportAfter(target.getEntity())) {
            this.exportEntities(alreadyExported, export, referenceMap, this.exportTarget(after));
        }
    }

    /**
     * Encapsula o objeto em um ExportTarget com action de INCLUIR, caso necessário. Caso o objeto já seja um ExportTarget, apenas retorna o
     * mesmo
     * 
     * @param target target
     * @return ExportTarget
     */
    private ExportTarget exportTarget(Object target) {
        return ExportTarget.class.isAssignableFrom(target.getClass()) ? (ExportTarget) target : new DefaultExportTarget(
                DefaultExportAction.INCLUIR, target);
    }

    /**
     * Chave que identifica uma exportação que já ocorreu
     * 
     * @author lundberg
     */
    private class ExportedKey {
        private ExportAction action;

        private Object entity;

        /**
         * Construtor
         * 
         * @param target target
         */
        public ExportedKey(ExportTarget target) {
            if (target.getAction() == null) {
                throw new NullPointerException("Null export action.");
            }
            if (target.getEntity() == null) {
                throw new NullPointerException("Null export target entity.");
            }
            this.action = target.getAction();
            this.entity = target.getEntity();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int hashCode() {
            return new HashCodeBuilder().append(this.action).append(System.identityHashCode(this.entity)).toHashCode();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (this.getClass() != obj.getClass()) {
                return false;
            }
            ExportedKey o = (ExportedKey) obj;
            return this.action.equals(o.action) && this.entity == o.entity;
        }
    }

    /**
     * @param exporterRepository the exporterRepository to set
     */
    public void setExporterRepository(EntityExporterRepository exporterRepository) {
        this.exporterRepository = exporterRepository;
    }
}
